import * as PIXI from 'pixi.js';
import { ref, computed, watch, nextTick } from 'vue';
import OverlimitDetail from './OverlimitDetail.jsx';
// import router from '@/router/index.js';

import { SideProp, calcPointerDownOffsetOn, calcPosBasedOn, globalBasedOn, isSelfTrigger } from './common.js';
import { typing } from './api/use-shortcut-key.js';
import { useDrag } from './api/use-drag.js';
import { setCrosshairColor, useCrosshair } from './api/use-crosshair.js';

import { addLabel, drawEyeCatcher, drawLabel, drawRectBox, makeEyeCatching, useRectBox } from './api/use-rect-box.js';
import { addCtrlPoint, siblingOf, getActivePosName, toGlobal, drawCtrlPointMap, setCtrlPointVisible, ToChange, getOppoPointName } from './api/use-ctrl-point.js';
import { CustomPosLimit, DragCtrlPointListener, calcPosAndSize } from './api/use-active-point.js';

import { storePkgInfo, CustomToQues, LabelFromStorableOf, categoryFromStorable } from './pkg-state.js';
import { Layer, existIn } from './api/use-layer.js';
import { Comment } from './api/use-comment.js';
import { makeFileIdValid } from '@/util/index.js';
import { setIndex } from './api/use-switch-index.js';
import { Modal } from 'ant-design-vue';
import * as storage from '@/util/storage.js';
// import { message } from 'ant-design-vue';

/**
 * 加载并处理必要数据
 * @param {{ getFileList: Function; } & Pick<ObjDetn.Ctx, 'quesIndex' | 'img' | 'textures' | 'classes' | 'imageClasses' | 'labelColorDic' | 'quesList' | 'autoAppendLayersExpected'>} ctx
 */
export function InitAppOf(ctx) {
  /** @param {ObjDetn.PkgInfo} pkgInfo 记录进入标注页前所点击的，数据详情页的用户、题包和题 id */
  return async function(pkgInfo) {
    // 避免跨包发生自动追加
    ctx.autoAppendLayersExpected.value.clear();

    ctx.loading.value = true;
    // const fileList = await ctx.getFileList(pkgInfo);

    const fileList = [
      {
        assignedFileId: 1,
        assignedLabels: '',
        assignedImageCategories: '',
        assignedLabelsText: '',
        assignedImageCategoriesText: '',
        auditStatus: null,
        auditTime: '',
        auditUserId: null,
        auditUserName: null,
        comments: [],
        fileId: '1',
        fileName: '1',
        fileUrl: '/obj-detn/2f536c3b-3492-4da5-a48c-0525e4ae158b_1745122139466662631~tplv-a9rns2rl98-web-thumb-watermark.jpeg',
        id: 1,
        // img: PIXI.Sprite;
        note: null,
        packageId: 1,
        rejectReason: null,
        remark: null,
        submitTime: null,
        taskId: 1,
        userId: 1,
      },
    ];

    // const newPkgInfo = makeFileIdValid(pkgInfo, fileList);
    const newPkgInfo = { user: 1, pkg: 1, ques: 1 };
    storage.writeAsStr('pkgInfo', newPkgInfo);

    ctx.quesIndex.value = R.findIndex(R.propEq(newPkgInfo.ques, 'id'), fileList);
    ctx.img.value.texture = await PIXI.Texture.fromURL(/* $config.urlDic.file + */ fileList[ctx.quesIndex.value].fileUrl);

    const Texs = R.map(R.compose( PIXI.Texture.fromURL, /* R.concat($config.urlDic.file), */ R.prop('fileUrl') ));
    ctx.textures.value = await Promise.all(Texs(fileList));

    // - [ ] 处理请求失败的纹理
    // .catch(error => {
    //   message.warning('图片加载失败');
    //   router.back();
    // });

    ctx.quesList.value = fileList.map(
      CustomToQues({
        Label: LabelFromStorableOf(ctx),
        Category: categoryFromStorable(ctx.compatibleImageClasses)
      })
    );

    ctx.loading.value = false;
  };
};

/**
 * 所有变量函数聚集在此大联欢
 * @param {ObjDetn.Ctx} ctx 
 */
export async function DataLabel(ctx) {
  function resetDataItem() {
    ctx.editingComment.value = null;
    ctx.editingLayer.value = null;
    ctx.activeLayer.value = null;
    ctx.selectLayer('single', 'switch', null); // 避免 watch(activeDataItem) 不触发
  }

  function initPct() {
    // 比最小百分比再小点儿，留出的空间用于显示超出图片的标签
    ctx.minPercentage.value = ctx.calcPctAtContain(ctx.img.value) - 5;
    ctx.percentage.value = ctx.minPercentage.value;
    ctx.maxPercentage.value = Math.max(300, ctx.minPercentage.value);
  }

  const LabelsAppendable = R.compose(
    R.pluck(0),
    ctx.OverlimitSatisfies( R.equals(0), ctx.OverlimitCountForQues.value, ctx.LabelCountPairsForExtr )
  );

  const LayersFilterBy = f => R.converge(
    (ss, xs) => R.filter(
      R.compose( R.includes(R.__, ss), R.path(['label', 0]) ),
      xs
    ),
    [f, R.identity]
  );

  // 粘贴和自动追加，拒收标签已满的 Layer
  // 表格数据 LabelCoutnPair[]，故 dataIndex 用下标
  function labelsCountOverlimitWarn(dataSource) {
    if (!dataSource.length) { return; }

    ctx.overlimitModalProps.value.visible = true;
    Modal.warning({
      width: 1000,
      centered: true,
      title: '标签超量',
      content: OverlimitDetail(
        { dataSource },
        { slots: { description: () => '部分标签超出限定选择数量，详情如下：' } }
      ),
      onOk: () => (ctx.overlimitModalProps.value.visible = false),
    });
  }
  
  const Tex = ques => R.compose( R.prop(R.__, ctx.textures.value), R.indexOf(R.__, ctx.quesList.value) )(ques);
  
  const ValidAutoAppend = R.compose(
    R.map(ctx.cloneLayer),
    LayersFilterBy(LabelsAppendable),
    R.tap(R.pipe(
      ctx.OverlimitSatisfies( R.gt(R.__, 0), ctx.OverlimitCountForQues.value, ctx.LabelCountPairsForExtr ),
      labelsCountOverlimitWarn
    )),
    R.reject(existIn(ctx.layers)),
    Array.from
  );

  // 未使用工具时点击图片，取消选择所有 dataItem
  const body = initBody(R.both( R.allPass([ctx.isNotUsingTool, isSelfTrigger]), resetDataItem ));

  /** 拖拽 body 避免超出工作区 */
  const dragLimitToScreen = CustomPosLimit(side => [
    0 - body[SideProp(side)] + 24,
    ctx.app.screen[SideProp(side)] - 24,
  ]);

  /** 画框 和 拖拽控制点，Point 限制在图片范围内 */
  const pointerLimitToImg = CustomPosLimit(side => [
    0,
    ctx.img.value[SideProp(side)],
  ]);

  // 标注框生成和绘制流程
  const { container: containerOfLayers, update: updateOfLayers } = useRectBox({
    xs: ctx.layers,
    flow: R.ifElse(
      () => ctx.OpGte('标注'),
      () => R.pipe(
        ({ xs, rectBox }) => {
          rectBox.eventMode = 'static';
          return { xs, rectBox };
        },
        makeEyeCatching({
          pointerenter: { pred: R.T, f: setCtrlPointVisible(true) },
          pointerleave: {
            pred: dataItem => !ctx.selectedLayers.value.has(dataItem),
            f: setCtrlPointVisible(false),
          }
        }),
        addLabel,
        addCtrlPoint(({ xs, ctrlPoint }) => {
          const listenerMap = useDrag({
            app: ctx.app,
            calcStartPoint: () => R.compose( globalBasedOn(body), toGlobal, siblingOf(ctrlPoint), getActivePosName )(ctrlPoint.name),
            elem: ctrlPoint,
            pred: () => ctx.isNotUsingTool(),
          });

          
          listenerMap.pointerdown = ({ event, startPoint }) => {
            event.stopPropagation();

            const dataItem = R.find(R.propEq(ctrlPoint.parent.id, 'id'), xs.value);
            listenerMap.data = dataItem;
            ctx.activeLayer.value = dataItem;

            const oppoPos = R.compose( globalBasedOn(body), toGlobal, siblingOf(ctrlPoint), getOppoPointName, getActivePosName )(ctrlPoint.name);
            listenerMap.moveListener = R.compose(
              calcPosAndSize(oppoPos),
              pointerLimitToImg,
              DragCtrlPointListener({
                startPoint,
                calcGlobalPoint: R.compose( globalBasedOn(body), R.prop('global') ),
                toChange: ToChange(ctrlPoint.name),
              })
            );
          };

          listenerMap.pointermove = ({ event }) => {
            Object.assign(listenerMap.data, listenerMap.moveListener(event));
          };
        }),
        ({ xs, rectBox }) => {
          // makeDraggable
          const listenerMap = useDrag({
            app: ctx.app,
            calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
            elem: rectBox,
            pred: () => ctx.isNotUsingTool(),
          });

          listenerMap.pointerdown = ({ event }) => {
            const dataItem = R.find(R.propEq(rectBox.id, 'id'), xs.value);
            listenerMap.data = dataItem;
            ctx.activeLayer.value = dataItem;
            listenerMap.moveListener = R.compose( calcPosBasedOn, calcPointerDownOffsetOn(event.currentTarget), globalBasedOn(body) )(event.global)
          };

          listenerMap.pointermove = ({ event }) => {
            R.either(
              R.prop('ctrlKey'),
              () => Object.assign(
                listenerMap.data,
                R.compose(
                  CustomPosLimit(side => [
                    0,
                    ctx.img.value[SideProp(side)] - listenerMap.data[SideProp(side)],
                  ]),
                  listenerMap.moveListener,
                  globalBasedOn(body)
                )(event.global)
              )
            )(event);
          };

          return { xs, rectBox };
        },
      ),
      () => R.pipe(
        ({ xs, rectBox }) => {
          rectBox.eventMode = 'static';
          return { xs, rectBox };
        },
        makeEyeCatching({
          pointerenter: { pred: R.T, f: R.identity },
          pointerleave: { pred: R.T, f: R.identity },
        }),
        addLabel,
        ({ xs, rectBox }) => {
          // 按下矩形显示醒目块
          const listenerMap = useDrag({
            app: ctx.app,
            calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
            elem: rectBox,
            pred: () => ctx.isNotUsingTool(),
          });

          listenerMap.pointerdown = ({ event }) => {
            const dataItem = R.find(R.propEq(rectBox.id, 'id'), xs.value);
            ctx.activeLayer.value = dataItem;
          };

          return { xs, rectBox };
        },
      ),
    )(),
    reDraw: R.ifElse(
      () => ctx.OpGte('标注'),
      R.pipe(
        drawEyeCatcher(() => ctx.appOpts.value.eyeCatcher.alpha),
        drawRectBox(() => ctx.calcOnPct(1)),
        drawLabel(({ dataItem }) => ({
          y: ctx.calcOnPct(-14),
          style: {
            wordWrapWidth: ctx.longestLabelLength.value * ctx.calcOnPct(16),
            fontSize: ctx.calcOnPct(16),
            padding: ctx.calcOnPct(2),
            dropShadowColor: dataItem.color,
          }
        })),
        drawCtrlPointMap(() => ctx.calcOnPct(ctx.appOpts.value.ctrlPoint.r)),
      ),
      R.pipe(
        drawEyeCatcher(() => ctx.appOpts.value.eyeCatcher.alpha),
        drawRectBox(() => ctx.calcOnPct(1)),
        drawLabel(({ dataItem }) => ({
          y: ctx.calcOnPct(-14),
          style: {
            wordWrapWidth: ctx.longestLabelLength.value * ctx.calcOnPct(16),
            fontSize: ctx.calcOnPct(16),
            padding: ctx.calcOnPct(2),
            dropShadowColor: dataItem.color,
          }
        })),
      ),
    )
  });

  // 批注框生成和绘制流程
  const { container: containerOfComments, update: updateOfComments } = useRectBox({
    xs: ctx.comments,
    flow: R.ifElse(
      () => ctx.OpGte('审核'),
      () => R.pipe(
        ({ xs, rectBox }) => {
          rectBox.eventMode = 'static';
          return { xs, rectBox };
        },
        makeEyeCatching({
          pointerenter: { pred: R.T, f: setCtrlPointVisible(true) },
          pointerleave: {
            pred: dataItem => ctx.editingComment.value != dataItem,
            f: setCtrlPointVisible(false),
          },
        }),
        addCtrlPoint(({ xs, ctrlPoint }) => {
          const listenerMap = useDrag({
            app: ctx.app,
            calcStartPoint: () => R.compose( globalBasedOn(body), toGlobal, siblingOf(ctrlPoint), getActivePosName )(ctrlPoint.name),
            elem: ctrlPoint,
            pred: () => ctx.isNotUsingTool(),
          });
          
          
          listenerMap.pointerdown = ({ event, startPoint }) => {
            event.stopPropagation();

            const dataItem = R.find(R.propEq(ctrlPoint.parent.id, 'id'), xs.value);
            listenerMap.data = dataItem;
            ctx.selectComment(dataItem);

            const oppoPos = R.compose( globalBasedOn(body), toGlobal, siblingOf(ctrlPoint), getOppoPointName, getActivePosName )(ctrlPoint.name);
            listenerMap.moveListener = R.compose(
              calcPosAndSize(oppoPos),
              pointerLimitToImg,
              DragCtrlPointListener({
                startPoint,
                calcGlobalPoint: R.compose( globalBasedOn(body), R.prop('global') ),
                toChange: ToChange(ctrlPoint.name),
              })
            );
          };

          listenerMap.pointermove = ({ event }) => {
            Object.assign(listenerMap.data, listenerMap.moveListener(event));
          };
        }),
        ({ xs, rectBox }) => {
          // makeDraggable
          const listenerMap = useDrag({
            app: ctx.app,
            calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
            elem: rectBox,
            pred: () => ctx.isNotUsingTool(),
          });

          listenerMap.pointerdown = ({ event }) => {
            const dataItem = R.find(R.propEq(rectBox.id, 'id'), xs.value);
            listenerMap.data = dataItem;
            ctx.selectComment(dataItem);
            listenerMap.moveListener = R.compose( calcPosBasedOn, calcPointerDownOffsetOn(event.currentTarget), globalBasedOn(body) )(event.global);
          };

          listenerMap.pointermove = ({ event }) => {
            R.either(
              R.prop('ctrlKey'),
              () => Object.assign(
                listenerMap.data,
                R.compose(
                  CustomPosLimit(side => [
                    0,
                    ctx.img.value[SideProp(side)] - listenerMap.data[SideProp(side)],
                  ]),
                  listenerMap.moveListener,
                  globalBasedOn(body)
                )(event.global)
              )
            )(event);
          };

          return { xs, rectBox };
        }
      ),
      () => R.pipe(
        ({ xs, rectBox }) => {
          rectBox.eventMode = 'static';
          return { xs, rectBox };
        },
        makeEyeCatching({
          pointerenter: { pred: R.T, f: R.identity },
          pointerleave: { pred: R.T, f: R.identity },
        }),
        ({ xs, rectBox }) => {
          // 按下矩形显示醒目块
          const listenerMap = useDrag({
            app: ctx.app,
            calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
            elem: rectBox,
            pred: () => ctx.isNotUsingTool(),
          });

          listenerMap.pointerdown = ({ event }) => {
            const dataItem = R.find(R.propEq(rectBox.id, 'id'), xs.value);
            ctx.selectComment(dataItem);
          };

          return { xs, rectBox };
        }
      ),
    )(),
    reDraw: R.ifElse(
      () => ctx.OpGte('审核'),
      R.pipe(
        drawEyeCatcher(() => ctx.appOpts.value.eyeCatcher.alpha),
        drawRectBox(() => ctx.calcOnPct(1)),
        drawCtrlPointMap(() => ctx.calcOnPct(ctx.appOpts.value.ctrlPoint.r)),
      ),
      R.pipe(
        drawEyeCatcher(() => ctx.appOpts.value.eyeCatcher.alpha),
        drawRectBox(() => ctx.calcOnPct(1)),
      ),
    )
  });

  /** 标注 z-index 重排序，使选中的层级高 */
  function reorderOfLayers() {
    // 已选数据对应的框才显示醒目块和控制点
    containerOfLayers.children.forEach(rectBox => {
      rectBox.eyeCatcher.visible = false;
      ctx.OpGte('标注') && setCtrlPointVisible(false, rectBox);
    });

    Array
    .from(ctx.selectedLayers.value)
    .reduce((xs, dataItem) => {
      const rectBox = R.find(R.propEq(dataItem.id, 'id'), containerOfLayers.children);
      rectBox && xs.push({ dataItem, rectBox });
      return xs;
    }, [])
    .forEach(({ dataItem, rectBox }, i, xs) => {
      // 下标越靠后层级越高
      containerOfLayers.setChildIndex(rectBox, containerOfLayers.children.length - xs.length + i);
      rectBox.eyeCatcher.visible = true;
      drawEyeCatcher(() => ctx.appOpts.value.eyeCatcher.alpha)({ dataItem, rectBox });

      if (ctx.OpGte('标注')) {
        drawCtrlPointMap(() => ctx.calcOnPct(ctx.appOpts.value.ctrlPoint.r))({ dataItem, rectBox });
        setCtrlPointVisible(true, rectBox);
      }
    });
  }

  /** 批注 z-index 重排序，使选中的层级高 */
  function reorderOfComments() {
    // 已选数据对应的框才显示醒目块和控制点
    containerOfComments.children.forEach(rectBox => {
      rectBox.eyeCatcher.visible = false;
      ctx.OpEq('审核') && setCtrlPointVisible(false, rectBox);
    });

    [ctx.editingComment.value]
    .filter(Boolean)
    .reduce((xs, dataItem) => {
      const rectBox = R.find(R.propEq(dataItem.id, 'id'), containerOfComments.children);
      rectBox && xs.push({ dataItem, rectBox });
      return xs;
    }, [])
    .forEach(({ dataItem, rectBox }, i, xs) => {
      rectBox.eyeCatcher.visible = true;
      drawEyeCatcher(() => ctx.appOpts.value.eyeCatcher.alpha)({ dataItem, rectBox });

      if (ctx.OpEq('审核')) {
        drawCtrlPointMap(() => ctx.calcOnPct(ctx.appOpts.value.ctrlPoint.r))({ dataItem, rectBox });
        setCtrlPointVisible(true, rectBox);
      }
    });
  }

  function updateStage() {
    updateOfLayers();
    updateOfComments();
    reorderOfLayers();
    reorderOfComments();
  }

  // 标注、批注和缩放变化需更新舞台
  watch([ctx.percentage, ctx.appOpts], updateStage, { deep: true });
  watch(ctx.layers, R.pipe( updateOfLayers, reorderOfLayers ), { deep: true });
  watch(ctx.comments, R.pipe( updateOfComments, reorderOfComments ), { deep: true });
  watch(ctx.editingComment, reorderOfComments, { deep: true });
  watch(() => Array.from(ctx.selectedLayers.value), reorderOfLayers, { deep: true });

  await ctx.initApp(storage.readAsObj('pkgInfo'));
  ctx.copiedLayers.value = storage.readAsObj('copiedDataItems');

  ctx.listenShortcutKey();

  ctx.addAllowDefault(() => ctx.overlimitModalProps.value.visible);
  ctx.addPreCond(() => !ctx.overlimitModalProps.value.visible);

  ctx.addAllowDefault(() => ctx.kbdModalProps.value.visible);
  ctx.addPreCond(() => !ctx.kbdModalProps.value.visible);
  watch(() => ctx.kbdModalProps.value.visible, value => value && (ctx.using.value = 'none'));

  watch(ctx.ques, (value, oldValue) => {
    resetDataItem();
    ctx.lastSelectIndex.value = 0;
    ctx.saveFile(oldValue);
    storePkgInfo(value);

    // 换图后最小百分比未必变化，故手动赋值 percentage.value
    ctx.img.value.texture = Tex(value);
    initPct();

    // 换题后，自动追加标注只留下不存在的
    // 当前题的标注再追加深拷贝后不存在的标注
    const autoAppendLayers = ValidAutoAppend(ctx.autoAppendLayersExpected.value);
    ctx.autoAppendLayersExpected.value = new Set(autoAppendLayers);
    ctx.layers.value.push( ...autoAppendLayers );
  });

  // 判定题目中的标注是否修改过
  watch(
    ctx.layers,
    (newXs, oldXs) => {
      // 同一题内增删改或换题后自动追加的即为改过
      nextTick()
      .then(() => {
        ctx.changed.value = newXs == oldXs || 0 < ctx.autoAppendLayersExpected.value.size;
      });
    },
    { deep: true }
  );

  // 判定题目中的分类是否修改过
  watch(
    ctx.categories,
    (newXs, oldXs) => {
      // 同一题内增删改即为改过
      nextTick()
      .then(() => {
        ctx.changed.value = newXs == oldXs || newXs.length != oldXs.length;
      });
    },
    { deep: true }
  )

  // 上下箭头切换选择标注
  function useSwitchSelect({ Pred, Listener }) {
    ctx.addShortcutKey(['ArrowUp'], Listener('prev'), Pred('prev'));
    ctx.addShortcutKey(['ArrowDown'], Listener('next'), Pred('next'));
  }

  useSwitchSelect({
    Pred: dir => () => 'switch' == ctx.arrowAction.value && ctx.selectSwitchable.value[dir],
    Listener: dir => () => ctx.selectLayer('single', 'switch', ctx.findSibling(dir))
  });

  // 工作区
  // app.stage.children [touchPad, body, crosshair]
  // body.children [img, containerOfLayers, containerOfComments]

  const workspace = document.getElementById('workspace');
  ctx.app.resizeTo = workspace;
  ctx.app.resize();
  ctx.app.stage.eventMode = 'static';
  workspace.appendChild(ctx.app.view);

  const touchPad = new PIXI.Graphics();
  touchPad.beginFill('rgb(128,128,128)', 1);
  touchPad.drawRect(0, 0, ctx.app.screen.width, ctx.app.screen.height);
  touchPad.endFill();
  ctx.app.stage.addChild(touchPad);

  // 基础功能：换题、缩放和拖拽视图
  ctx.addShortcutKey(['a'], () => setIndex(ctx.quesIndex, 'prev'), () => ctx.quesSwitchable.value.prev);
  ctx.addShortcutKey(['d'], () => setIndex(ctx.quesIndex, 'next'), () => ctx.quesSwitchable.value.next);

  // 以鼠标中心缩放
  const wheelEventPoint = ref({ x: 0, y: 0 });
  watch(ctx.percentage, () => {
    // 旧坐标 - 新旧尺寸差 * 滚轮触发位置的百分比
    const eventPosPct = { x: wheelEventPoint.value.x / body.width, y: wheelEventPoint.value.y / body.height };
    const oldPos = R.pick(['x', 'y'], body);
    const oldSize = R.pick(['width', 'height'], body);
    body.scale.set(ctx.percentage.value / 100);
    const newSize = R.pick(['width', 'height'], body);
    
    const sizeDiff = { width: newSize.width - oldSize.width, height: newSize.height - oldSize.height };
  
    body.position.set(
      oldPos.x - sizeDiff.width * eventPosPct.x,
      oldPos.y - sizeDiff.height * eventPosPct.y,
    );

    // 缩到最小百分比就挪到屏幕中间
    if (ctx.percentage.value <= ctx.minPercentage.value) {    
      Object.assign(
        body,
        ctx.elemPosInCenterOfScreen({
          width: ctx.img.value.width * body.scale.x,
          height: ctx.img.value.height * body.scale.y
        })
      );
    }
  });

  // 图片大小快捷键
  (() => {
    ctx.addShortcutKey(['1', 'ctrl'], () => ctx.percentage.value = 100);
    ctx.addShortcutKey(['2', 'ctrl'], () => ctx.percentage.value = ctx.minPercentage.value);
    ctx.addShortcutKey(['=', 'ctrl'], () => ctx.scaleChangeTo('big'));
    ctx.addShortcutKey(['+', 'ctrl'], () => ctx.scaleChangeTo('big'));
    ctx.addShortcutKey(['-', 'ctrl'], () => ctx.scaleChangeTo('small'));

    function listenWheel() {
      const prevent = event => (event.preventDefault(), event);
      const willChange = R.ifElse( R.lte(R.__, 0), R.always('big'), R.always('small') );

      function handle(event) {
        Object.assign(wheelEventPoint.value, { x: event.global.x - body.x, y: event.global.y - body.y });
        R.compose( ctx.scaleChangeTo, willChange, R.prop('deltaY') )(event);
      }

      // 阻止默认 ctrl + wheel，对 body ctrl + wheel 可缩放
      addEventListener('wheel', R.both(R.prop('ctrlKey'), prevent), { passive: false });
      body.on('wheel', R.both(R.prop('ctrlKey'), handle));
    }

    listenWheel();
  })();

  // 拖拽图片快捷键
  (() => {
    ctx.addShortcutKey([' '], ctx.toggleUsingDrag);

    const listenerMap = useDrag({
      app: ctx.app,
      calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
      elem: body,
      pred: () => ctx.isUsing('drag'),
    });

    listenerMap.pointerdown = ({ event }) => {
      listenerMap.moveListener = R.compose( calcPosBasedOn, calcPointerDownOffsetOn(event.currentTarget) )(event.global);
    }
  
    listenerMap.pointermove = ({ event }) => {
      ctx.using.value = 'dragging';      
      Object.assign(body, R.compose( dragLimitToScreen, listenerMap.moveListener )(event.global));
    };
  
    listenerMap.pointerup = () => ctx.using.value = 'drag';
  })();

  watch(ctx.using, value => {
    const Cursor = R.prop(
      R.__,
      {
        none: 'default',
        drag: 'grab',
        dragging: 'grabbing',
        rectBox: 'crosshair',
        comment: 'crosshair',
      }
    );

    body.cursor = Cursor(value);
  });

  ctx.app.stage.addChild(body);
  body.addChild(ctx.img.value);
  body.addChild(containerOfLayers);
  body.addChild(containerOfComments);

  initPct();

  // 十字参考线
  const { crosshair, start, stop } = useCrosshair(ctx.app);
  ctx.app.stage.addChild(crosshair);

  const isUsingRectTool = computed(() => R.includes(ctx.using.value, ['rectBox', 'comment']));
  watch(isUsingRectTool, value => {
    if (value) {
      const Color = R.prop(R.__, { rectBox: '#fff', comment: '#f00' });
      setCrosshairColor(Color(ctx.using.value));
    }
  });

  const needShowCrosshair = computed(() => isUsingRectTool.value && ctx.appOpts.value.crosshair.visible);
  watch(needShowCrosshair, R.ifElse( Boolean, start, stop ));

  R.compose( R.forEach(ctx.addAllowDefault), R.map(R.propEq(R.__, 'key', R.__)) )(['F5', 'F11', 'F12']);

  ctx.addAllowDefault(typing);
  ctx.addPreCond(R.complement(typing));

  ctx.addShortcutKey(
    ['Escape'],
    () => (ctx.using.value = 'none'),
    () => ctx.isUsingTool(ctx.using.value)
  );

  ctx.addShortcutKey(
    ['Escape'],
    resetDataItem,
    () => ctx.isNotUsingTool() && 0 < ctx.selectedLayers.value.size
  );

  ctx.addShortcutKey(['h', 'ctrl'], ctx.toggleVisibleOfLayers);

  const permsFns = ctx.addPerms({
    '标注': [
      () => {
        // use-layer
        const rectLimitToImg = CustomPosLimit(side => [
          0,
          ctx.img.value[SideProp(side)] - ctx.activeLayer.value[SideProp(side)],
        ]);

        ctx.addShortcutKey(
          ['ArrowLeft'],
          () => Object.assign( ctx.activeLayer.value, R.compose( rectLimitToImg, ctx.kbdMove )({ x: -1, y: 0 }) ),
          () => 'edit' == ctx.arrowAction.value,
        );
        ctx.addShortcutKey(
          ['ArrowUp'],
          () => Object.assign( ctx.activeLayer.value, R.compose( rectLimitToImg, ctx.kbdMove )({ x: 0, y: -1 }) ),
          () => 'edit' == ctx.arrowAction.value,
        );
        ctx.addShortcutKey(
          ['ArrowRight'],
          () => Object.assign( ctx.activeLayer.value, R.compose( rectLimitToImg, ctx.kbdMove )({ x: 1, y: 0 }) ),
          () => 'edit' == ctx.arrowAction.value,
        );
        ctx.addShortcutKey(
          ['ArrowDown'],
          () => Object.assign( ctx.activeLayer.value, R.compose( rectLimitToImg, ctx.kbdMove )({ x: 0, y: 1 }) ),
          () => 'edit' == ctx.arrowAction.value,
        );

        ctx.addShortcutKey(['a', 'ctrl'], () => ctx.selectLayer('single', 'switch', 'all'));
        ctx.addShortcutKey(['w'], () => ctx.using.value = 'rectBox');
        ctx.addShortcutKey(['c', 'ctrl'], ctx.copyLayers);
        ctx.addShortcutKey(
          ['v', 'ctrl'],
          () => {
            R.pipe(
              ctx.OverlimitSatisfies( R.gt(R.__, 0), ctx.OverlimitCountForQues.value, ctx.LabelCountPairsForExtr ),
              R.tap(labelsCountOverlimitWarn)
            )(ctx.copiedLayers.value);
            ctx.pasteLayers( LayersFilterBy(LabelsAppendable) );
          },
          () => 0 < ctx.copiedLayers.value.length && ctx.isNotUsingTool()
        );

        ctx.addShortcutKey(['Delete'], () => {
          ctx.selectedLayers.value.has(ctx.editingLayer.value) && (ctx.editingLayer.value = null);
          ctx.selectedLayers.value.has(ctx.activeLayer.value) && (ctx.activeLayer.value = null);
          Array.from(ctx.selectedLayers.value).forEach(ctx.delLayer);
          ctx.selectedLayers.value.clear();
        });

        const listenerMap = useDrag({
          app: ctx.app,
          calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
          elem: body,
          pred: () => ctx.isUsing('rectBox'),
        });

        listenerMap.pointerdown = ({ event, startPoint }) => {
          ctx.activeLayer.value = Layer(startPoint);
          ctx.layers.value.push(ctx.activeLayer.value);

          listenerMap.moveListener = R.compose(
            calcPosAndSize(startPoint),
            pointerLimitToImg,
            DragCtrlPointListener({
              startPoint,
              calcGlobalPoint: R.compose( globalBasedOn(body), R.prop('global') ),
            })
          );
        };

        listenerMap.pointermove = ({ event }) => {
          R.either(
            R.prop('ctrlKey'),
            () => Object.assign(ctx.activeLayer.value, listenerMap.moveListener(event))
          )(event);
        };


        listenerMap.pointerup = () => {
          ctx.using.value = 'none';
          // // 显示标签选择器的同时，标签排序
          // ctx.sortClasses({
          //   dataItem: ctx.activeLayer.value,
          //   imgElem: ctx.img.value.texture.baseTexture.resource.source,
          // });
          ctx.labelSelectorProps.value.visible = true;
        };
      },
      () => {
        // pkg-state
        ctx.addShortcutKey(['s', 'ctrl'], () => ctx.saveFile(ctx.ques.value));
      },
    ],
    '审核': [
      () => {
        // use-comment
        ctx.addShortcutKey(['i'], () => ctx.using.value = 'comment');

        ctx.addShortcutKey(
          ['Escape'],
          () => ctx.editingComment.value = null,
          () => ctx.isNotUsingTool() && ctx.editingComment.value,
        );

        const listenerMap = useDrag({
          app: ctx.app,
          calcStartPoint: R.compose( globalBasedOn(body), R.prop('global') ),
          elem: body,
          pred: () => ctx.isUsing('comment'),
        });

        listenerMap.pointerdown = ({ event, startPoint }) => {
          ctx.editingComment.value = Comment(startPoint);
          ctx.comments.value.push(ctx.editingComment.value);

          listenerMap.moveListener = R.compose(
            calcPosAndSize(startPoint),
            pointerLimitToImg,
            DragCtrlPointListener({
              startPoint,
              calcGlobalPoint: R.compose( globalBasedOn(body), R.prop('global') ),
            })
          );
        };

        listenerMap.pointermove = ({ event }) => {
          R.either(
            R.prop('ctrlKey'),
            () => Object.assign(ctx.editingComment.value, listenerMap.moveListener(event))
          )(event);
        };

        listenerMap.pointerup = () => {
          ctx.using.value = 'none';
          ctx.commentModalProps.value.visible = true;
        };
      },
      () => {
        ctx.addAllowDefault(() => ctx.judgeModalProps.value.visible);
        ctx.addPreCond(() => !ctx.judgeModalProps.value.visible);
        ctx.addAllowDefault(() => ctx.commentModalProps.value.visible);
        ctx.addPreCond(() => !ctx.commentModalProps.value.visible);
        ctx.addPreCond(() => !ctx.confirmModalProps.value.visible);

        ctx.addShortcutKey(['g'], ctx.judgePassQues);
        ctx.addShortcutKey(['h'], ctx.handleReject);
      },
    ],
  });
  
  permsFns();
}

function initBody(handlePointerdown) {
  const body = new PIXI.Container();
  body.eventMode = 'static';
  body.on('pointerdown', handlePointerdown);

  return body;
}
